Skip to content

feat: 휴지통 템플릿 영구 삭제 · 비우기 API - #78

Merged
oTAETAEo merged 6 commits into
developfrom
feat/70-trash-purge
Nov 9, 2026
Merged

oTAETAEo merged 6 commits into
developfrom
feat/70-trash-purge

Conversation

@oTAETAEo

@oTAETAEo oTAETAEo commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

관련 이슈

기준 브랜치

#69(16번 복구) 위에 쌓아 시작했고, #69가 병합되어 기준이 develop으로 바뀌었습니다.
develop(#63 갤러리 저장 포함)을 merge로 합쳤고, 충돌은 없었습니다. 합친 상태로 ./gradlew clean build 통과 (테스트 981건, 실패 0).

작업 내용

명세 17번 DELETE /api/v1/trash/templates/{templateId}(개별 영구 삭제)와 18번 DELETE /api/v1/trash/templates(휴지통 비우기)를 구현했습니다.

커밋별로 보기

  1. 삭제 쿼리: TemplateRepository.deleteTrashedByIdAndUserId, deleteTrashedByUserId (@Modifying JPQL delete, 지운 행 수 반환)
  2. 서비스: TrashCommandService.purge, empty, TrashPurgeCommand, TrashPurgeResult, TrashEmptyResult
  3. API: TrashController에 추가, TRASH200_3·TRASH200_4, TrashPurgeResponse, TrashEmptyResponse
  4. 수정: 영구 삭제·비우기도 회원 행을 먼저 잠가 복구와의 교착 방지 + 동시성 테스트

동작

API 조건 결과
17번 id·user_id·deleted_at is not null로 삭제 1행이면 TRASH200_3 + templateId, 0행이면 TRASH404_1 (없는 ID, 다른 회원, 활성 템플릿, 이미 지운 템플릿)
18번 user_id·deleted_at is not null로 삭제 TRASH200_4 + deletedCount. 비어 있어도 성공(0)
  • 엔티티를 읽지 않고 삭제 쿼리로 지웁니다 (feat: 휴지통 목록 API (#54) #55 리뷰 합의). 엔티티를 읽으면 본문을 복호화하므로, 복호화에 실패하는 행도 지울 수 있어야 하기 때문입니다. 통합 테스트로 확인했습니다.
  • 행을 지우는 쿼리라 updated_at은 넣지 않습니다.
  • @Modifying(flushAutomatically = true, clearAutomatically = true): 재발급 토큰 레포지토리와 같습니다.
    • clear: 없으면 지운 행이 1차 캐시에 남아 같은 트랜잭션의 findById가 그 행을 돌려줍니다 (플래그를 빼고 실패하는 것을 확인함).
    • flush: 캐시를 비울 때 아직 내보내지 않은 변경이 사라지지 않게 합니다.
    • 호출 뒤에는 미리 읽어 둔 엔티티를 고쳐도 DB에 반영되지 않습니다.
  • templates를 참조하는 외래키는 없습니다 (ERD v0.8, 마이그레이션 확인).
  • 00:10 정리 배치가 같은 모양을 쓸 수 있게 삭제 쿼리를 작게 두었습니다. 배치는 이 PR 범위가 아닙니다.

이슈에 없던 결정: 영구 삭제·비우기도 회원을 먼저 잠근다

  • 비우기가 회원을 잠그지 않으면, 진행 중인 복구와 교착이 나서 비우기가 MySQLTransactionRollbackException(500)이 됩니다. 동시성 테스트로 재현했습니다.
    • 비우기는 idx_templates_user_deleted → 기본 키 순서로 행을 잠급니다.
    • 복구는 기본 키를 잠근 뒤, 저장할 때 같은 인덱스를 고칩니다.
  • 복구와 같이 lockUser를 먼저 불러 같은 회원의 휴지통 쓰기를 줄 세웠습니다. 개별 영구 삭제도 비우기와 같은 순서 문제가 있을 수 있어 함께 맞췄습니다.
  • 그래서 #69에서 "정리 배치·영구 삭제는 회원을 잠그지 않는다"고 적은 주석 두 곳(TemplateRepository, TrashCommandService.restore)과 테스트 주석 하나를 "정리 배치는"으로 고쳤습니다.
  • 회원을 먼저 잠그므로 없는 회원 번호로 받은 토큰이면 TRASH404_1 대신 USER404_1입니다. 복구와 같고, 명세 1장의 개발용 토큰 설명과도 맞습니다.

명세와 다른 점

없습니다. 응답 필드와 문구는 명세 17·18번 그대로입니다.

테스트

  • TrashPurgeControllerTest (테스트컨테이너 MySQL)
    • 17번: 성공 응답·필드·no-store, 그 행만 삭제. 없는 ID·다른 회원의 활성·휴지통 템플릿·내 활성 템플릿·두 번째 삭제 TRASH404_1(아무것도 지우지 않음), 숫자가 아닌 templateId COMMON400_1, 토큰 없음 COMMON401_1
    • 18번: 내 휴지통만 삭제하고 deletedCount 일치, 활성 템플릿·다른 회원 휴지통 유지, 비어 있으면 0, 토큰 없음 COMMON401_1
    • 복호화가 실패하는 행도 17·18번으로 삭제
    • 삭제 후 15번 휴지통 목록의 totalCount·항목, 지운 단축어로 새 템플릿 생성
  • TrashPurgeRepositoryTest: 조건별 지운 행 수, 깨진 본문 행, 1차 캐시 비우기, 쌓인 변경 내보내기
  • TrashCommandServicePurgeTest (모키토): 지운 행 수에 따른 결과·TRASH404_1, 엔티티 조회 없음, 회원 잠금 → 삭제 순서
  • TrashPurgeConcurrencyTest (실제 커밋)
    • 같은 템플릿 영구 삭제 2건 → 1건 성공, 1건 TRASH404_1
    • 복구와 영구 삭제를 동시에 → 하나만 성공, 나머지 TRASH404_1
    • 영구 삭제가 먼저 잡으면 복구는 기다렸다가 TRASH404_1 / 복구가 먼저 잡으면 영구 삭제가 TRASH404_1
    • 복구 중 비우기 → 500 없이 둘 다 끝나고 복구한 템플릿만 남음
  • ./gradlew clean build 통과 (develop 합친 뒤 테스트 981건, 실패 0)

변경량

develop 대비 +910 / −4줄 (프로덕션 +111 / −3, 테스트 +799 / −1). 400줄 목표를 넘는 것은 이슈 완료 조건의 통합·동시성 테스트 때문입니다. 프로덕션 코드는 약 110줄입니다.

정리 배치(00:10)에서 볼 것

  • 배치가 회원을 잠그지 않고 deleted_at 범위로 지우면 복구와 같은 교착이 날 수 있습니다. 행 단위로 끊어 지울지, 교착 시 재시도할지 정해야 합니다.

🤖 Generated with Claude Code

엔티티를 읽지 않고 본인 소유 + 휴지통 조건으로 지우는 삭제 쿼리 두 개를 추가한다.
본문 복호화에 실패하는 행도 지울 수 있어야 하기 때문이다.
지운 행이 1차 캐시에 남지 않게 실행 뒤 캐시를 비우고, 그 전에 쌓인 변경을 내보낸다.
TrashCommandService에 purge·empty를 추가한다. 영구 삭제는 지운 행이 0이면 TRASH404_1,
비우기는 지운 개수를 돌려주고 비어 있어도 성공이다.
DELETE /api/v1/trash/templates/{templateId} (TRASH200_3)와
DELETE /api/v1/trash/templates (TRASH200_4)를 추가한다.
비우기는 (user_id, deleted_at) 인덱스 → 기본 키 순서로 행을 잠그고, 복구는 기본 키를 잠근 뒤
저장할 때 같은 인덱스를 고친다. 둘이 겹치면 교착이 나서 비우기가 500이 되는 것을 동시성 테스트로 재현했다.
복구와 같이 회원 행을 먼저 잠가 같은 회원의 휴지통 쓰기를 줄 세운다. 개별 영구 삭제도 같은 순서로 맞춘다.
영구 삭제가 회원을 잠그지 않는다고 적은 복구 쪽 주석은 정리 배치만 남기도록 고친다.
@oTAETAEo oTAETAEo added feature 기능 개발 template 내 템플릿·휴지통 labels Nov 6, 2026
@oTAETAEo oTAETAEo self-assigned this Nov 6, 2026
@oTAETAEo oTAETAEo changed the title [feat] 휴지통 템플릿 영구 삭제 · 비우기 API feat: 휴지통 템플릿 영구 삭제 · 비우기 API Nov 7, 2026
Base automatically changed from feat/67-trash-restore to develop November 7, 2026 04:14
@oTAETAEo
oTAETAEo marked this pull request as ready for review November 7, 2026 06:09
chjpooh2002
chjpooh2002 previously approved these changes Nov 9, 2026

@chjpooh2002 chjpooh2002 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

리뷰 확인 부탁드립니다~!

/**
* 본인의 휴지통 템플릿을 비관적 쓰기 잠금으로 찾는다 (SELECT ... FOR UPDATE). 복구가 쓴다.
* 회원을 잠그지 않는 정리 배치·영구 삭제가 먼저 지우고 있으면 기다렸다가 빈 값을 받는다.
* 회원을 잠그지 않는 정리 배치가 먼저 지우고 있으면 기다렸다가 빈 값을 받는다.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

교착을 재현까지 해서 PR 설명에 남겨 주셔서 덕분에 배치 설계 방향이 잡혔어요. 정리 배치(00:10)는 스케줄러·셰드락 공통 설정과 함께 제가 맡아서 이슈로 진행해도 될까요?! 설명에 적어 주신 교착 내용과 이 줄의 주석 수정도 그 이슈에 옮겨 두겠습니다.

배치 PR을 올리면 휴지통 잠금 흐름을 가장 잘 아시니 리뷰를 부탁드릴게요!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네넴 확인 했습니당 PR 올라오면 말씀 주세요 !


List<String> results = runAtOnce(() -> restore(trashedId), () -> purge(trashedId));

assertThat(results).containsOnlyOnce(SUCCESS).contains("TRASH404_1");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

결과 코드만 확인하고 DB 상태는 확인하지 않아요. 복구가 성공했으면 템플릿이 남아 있고 휴지통 밖에 있는지, 삭제가 성공했으면 행이 없는지까지 확인하면, 응답과 실제 상태가 어긋나는 경우도 잡을 수 있을 것 같습니다!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋습니당 반영 하겠습니다 !

결과 코드만 보던 테스트에 성공한 쪽에 맞는 DB 상태 확인을 더한다.
복구가 성공했으면 템플릿이 휴지통 밖에 남아 있고, 영구 삭제가 성공했으면 행이 없어야 한다.
@oTAETAEo
oTAETAEo merged commit bc1bd51 into develop Nov 9, 2026
1 check passed
@oTAETAEo
oTAETAEo deleted the feat/70-trash-purge branch November 9, 2026 04:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature 기능 개발 template 내 템플릿·휴지통

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] 휴지통 템플릿 영구 삭제 · 비우기 API

2 participants